﻿//------------------------------------------------------------------------------
// Copyright © FRA & FV 2014
// All rights reserved
//------------------------------------------------------------------------------
// Binary Serialization SDK
//
// SVN revision information:
//   $Revision: 97 $
//------------------------------------------------------------------------------
using System;
using System.IO;
using System.Text;

namespace FRAFV.Binary.Serialization
{
	/// <summary>Reads primitive data types as binary values with specific byte order and
	///   null-terminated strings in a specific encoding.</summary>
	public class BinStringReader : BinReader
	{
		internal sealed class BinDecoder
		{
			internal static readonly Encoding UTF8 = Encoding.UTF8; //UTF-8
			internal static readonly Encoding UTF16LE = Encoding.Unicode; //UTF-16 little-endian
			internal static readonly Encoding UTF16BE = Encoding.BigEndianUnicode; //UTF-16 big-endian
			internal static readonly Encoding UTF32LE = Encoding.UTF32; //UTF-32 little-endian
			internal static readonly Encoding UTF32BE = new UTF32Encoding(true, true); //UTF-32 big-endian
			private static readonly byte[][] BOMs = new byte[][] {
				UTF8.GetPreamble(), //UTF-8
				UTF16LE.GetPreamble(), //UTF-16 little-endian
				UTF16BE.GetPreamble(), //UTF-16 big-endian
				UTF32LE.GetPreamble(), //UTF-32 little-endian
				UTF32BE.GetPreamble() //UTF-32 big-endian
			};
			private static readonly Encoding[] Encodings = new Encoding[] {
				UTF8, //UTF-8
				UTF16LE, //UTF-16 little-endian
				UTF16BE, //UTF-16 big-endian
				UTF32LE, //UTF-32 little-endian
				UTF32BE //UTF-32 big-endian
			};
			internal const int MaxCharBytesSize = 128;
			internal const int MaxBOMSize = 4;
			private Stream stream;
			private Decoder decoder;
			private byte[] charBytes;
			private char[] charBuffer;
			private int charSize;

			internal static int ResolveCharSize(Encoding encoding)
			{
				switch (encoding.CodePage)
				{
					case 1200:
					case 1201:
						return 2;
					case 12000:
					case 12001:
						return 4;
					default:
						return 1;
				}
			}

			public BinDecoder(Stream input, Encoding encoding)
			{
				this.stream = input;
				this.decoder = encoding.GetDecoder();
				this.charSize = ResolveCharSize(encoding);
				this.charBytes = new byte[MaxCharBytesSize];
				this.charBuffer = new char[encoding.GetMaxCharCount(MaxCharBytesSize)];
			}

			private bool IsZero(int start, int len)
			{
				for (int k = start; k < start + len; k++)
					if (charBytes[k] != 0)
						return false;
				return true;
			}

			private bool IsBOM(int len, byte[] bom)
			{
				if (bom.Length > len) return false;
				for (int k = 0; k < bom.Length; k++)
					if (charBytes[k] != bom[k])
						return false;
				return true;
			}

			public int DetectBOM(StringBuilder sb, bool zero, bool unterminated, ref BinDecoder reader)
			{
				decoder.Reset();
				int len, read, start = 0;
				for (len = 0; len < MaxBOMSize && start == 0; )
				{
					read = Math.Min(charSize, MaxBOMSize - len);
					read = stream.Read(charBytes, len, read);
					if (read == 0)
					{
						if (!zero || unterminated) break;
						throw new EndOfStreamException();
					}
					if (zero && IsZero(len, read)) break;
					len += read;
					for (int i = 0; i < BOMs.Length; i++)
					{
						var bom = BOMs[i];
						if (!IsBOM(len, bom)) continue;
						var e = Encodings[i];
						reader = new BinDecoder(stream, e);
						start = bom.Length;
						break;
					}
				}
				int convert = reader.decoder.GetChars(charBytes, start, len - start, reader.charBuffer, 0);
				sb.Append(reader.charBuffer, 0, convert);
				return len;
			}

			public int Read()
			{
				long save = stream.CanSeek ? stream.Position : 0L;

				int convert = 0;
				while (convert == 0)
				{
					int read = stream.Read(charBytes, 0, charSize);
					if (read == 0)
						return -1;

					try
					{
						convert = decoder.GetChars(charBytes, 0, read, charBuffer, 0);
					}
					catch (DecoderFallbackException)
					{
						if (stream.CanSeek)
							stream.Seek(save - stream.Position, SeekOrigin.Current);
						throw;
					}
				}
				return (int)charBuffer[0];
			}

			public int ReadString(StringBuilder sb, bool unterminated)
			{
				int len, read;
				for (len = 0; true; len += read)
				{
					read = stream.Read(charBytes, 0, charSize);
					if (read == 0)
					{
						if (unterminated) break;
						throw new EndOfStreamException();
					}

					int convert = decoder.GetChars(charBytes, 0, read, charBuffer, 0);
					if (convert == 0) continue;
					char c = charBuffer[0];
					if (c == '\0') break;
					sb.Append(c);
				}
				return len;
			}

			public int ReadChars(StringBuilder sb, int length)
			{
				int left, read;
				for (left = length; left > 0; left -= read)
				{
					read = Math.Min(left, MaxCharBytesSize);
					read = stream.Read(charBytes, 0, read);
					if (read == 0)
						throw new EndOfStreamException();

					int convert = decoder.GetChars(charBytes, 0, read, charBuffer, 0);
					if (convert > 0)
						sb.Append(charBuffer, 0, convert);
				}
				return length - left;
			}
		}

		private Encoding encoding;
		private BinDecoder decoder;

		/// <summary>Allows read an unterminated string, end of stream instead of null-terminator.</summary>
		public bool AllowUnterminatedString { get; set; }

		/// <summary>Allows the encoding will be changed by BOM detection.</summary>
		public bool AllowDetectBOM { get; set; }

		/// <summary>Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BinStringReader" /> class
		///   based on the supplied stream and using <see cref="T:System.Text.UTF8Encoding" />.</summary>
		/// <param name="input">The supplied stream.</param>
		/// <exception cref="T:System.ArgumentException">The stream does not support reading, the stream is null, or
		///   the stream is already closed. </exception>
		public BinStringReader(Stream input)
			: this(input, BinDecoder.UTF8, BitConverter.IsLittleEndian) { }

		/// <summary>Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BinStringReader" /> class
		///   based on the supplied stream and a specific character encoding.</summary>
		/// <param name="input">The supplied stream.</param>
		/// <param name="encoding">The character encoding. </param>
		/// <exception cref="T:System.ArgumentException">The stream does not support reading, the stream is null, or
		///   the stream is already closed. </exception>
		/// <exception cref="T:System.ArgumentNullException">
		///   <paramref name="encoding" /> is null. </exception>
		public BinStringReader(Stream input, Encoding encoding)
			: this(input, encoding, BitConverter.IsLittleEndian) { }

		/// <summary>Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BinStringReader" /> class
		///   based on the supplied stream with specific byte order and using <see cref="T:System.Text.UTF8Encoding" />.</summary>
		/// <param name="input">The supplied stream.</param>
		/// <param name="littleEndian">true to use the little-endian byte order (most significant byte first), or false
		///   to use the big-endian byte order (least significant byte first).</param>
		/// <exception cref="T:System.ArgumentException">The stream does not support reading, the stream is null, or
		///   the stream is already closed. </exception>
		public BinStringReader(Stream input, bool littleEndian)
			: this(input, BinDecoder.UTF8, littleEndian) { }

		/// <summary>Initializes a new instance of the <see cref="T:FRAFV.Binary.Serialization.BinStringReader" /> class
		///   based on the supplied stream with specific byte order and a specific character encoding.</summary>
		/// <param name="input">The supplied stream.</param>
		/// <param name="encoding">The character encoding. </param>
		/// <param name="littleEndian">true to use the little-endian byte order (most significant byte first), or false
		///   to use the big-endian byte order (least significant byte first).</param>
		/// <exception cref="T:System.ArgumentException">The stream does not support reading, the stream is null, or
		///   the stream is already closed. </exception>
		/// <exception cref="T:System.ArgumentNullException">
		///   <paramref name="encoding" /> is null. </exception>
		public BinStringReader(Stream input, Encoding encoding, bool littleEndian)
			: base(input, encoding, littleEndian)
		{
			this.encoding = encoding;
		}

		/// <summary>Reads characters from the underlying stream and advances the current position of the stream in 
		///   accordance with the encoding used and the specific character being read from the stream.</summary>
		/// <returns>The next character from the input stream, or <c>-1</c> if no characters are currently available.</returns>
		/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
		/// <exception cref="T:System.ObjectDisposedException">The stream is closed. </exception>
		public override int Read()
		{
			if (decoder == null)
				decoder = new BinDecoder(BaseStream, encoding);

			return decoder.Read();
		}

		/// <summary>Reads a null-terminated string string from the current stream.</summary>
		/// <returns>The string being read.</returns>
		/// <exception cref="T:System.IO.EndOfStreamException">The end of the stream is reached. </exception>
		/// <exception cref="T:System.ObjectDisposedException">The stream is closed. </exception>
		/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
		public override string ReadString()
		{
			if (decoder == null)
				decoder = new BinDecoder(BaseStream, encoding);

			var sb = new StringBuilder();
			var reader = this.decoder;
			if (!AllowDetectBOM ||
				reader.DetectBOM(sb, true, AllowUnterminatedString, ref reader) == BinDecoder.MaxBOMSize)
				reader.ReadString(sb, AllowUnterminatedString);
			return sb.ToString();
		}

		/// <summary>Reads the characters from the current stream by specified number of bytes, returns the data in
		///   a character array, and advances the current position in accordance with the encoding used and
		///   the specific character being read from the stream.</summary>
		/// <returns>A character array containing data read from the underlying stream.</returns>
		/// <param name="length">The number of bytes to read. </param>
		/// <exception cref="T:System.IO.EndOfStreamException">The end of the stream is reached. </exception>
		/// <exception cref="T:System.ArgumentException">A Unicode decoder returns fallback characters.</exception>
		/// <exception cref="T:System.ObjectDisposedException">The stream is closed. </exception>
		/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		///   <paramref name="length" /> is negative. </exception>
		public override char[] ReadChars(int length)
		{
			if (decoder == null)
				decoder = new BinDecoder(BaseStream, encoding);

			var sb = new StringBuilder();
			var reader = this.decoder;
			int read = 0;
			if (AllowDetectBOM)
				read = reader.DetectBOM(sb, false, false, ref reader);
			reader.ReadChars(sb, length - read);
			var result = new char[sb.Length];
			sb.CopyTo(0, result, 0, sb.Length);
			return result;
		}
	}
}
